package cn.itcast.cralwe_jd.task;

import cn.itcast.cralwe_jd.pojo.Item;
import cn.itcast.cralwe_jd.service.ItemService;
import cn.itcast.cralwe_jd.utils.HttpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;
import java.util.List;

/**
 * 初识spu和sku之间的关系：
 *      简单而言spu是从属于sku的，spu是商品物理上不能分割的最小表示码，举个简单的例子，假设今年小米出了
 *      一款xxx的手机，则这款手机就有了唯一一个spu表示码，但是该手机的内存大小，尺寸和颜色，作为同一款手
 *      机的不同参数，对应的价格也不相同，则sku = spu + 内存大小 + 尺寸 + 颜色，这就是spu和sku之间的
 *      关系
 *
 *
 * 抓取数据的定时任务
 * 分析实现思路，到底如何才能抓取到数据?(抓取的是华为手机的全部)
 *  首先我们先到京东官网上查看手机的页面地址，首地址如下：
 *  https://search.jd.com/Search?keyword=%E5%8D%8E%E4%B8%BA%E6%89%8B%E6%9C%BA&qrst=1&suggest=1.def.0.0&wq=%E5%8D%8E%E4%B8%BA%E6%89%8B%E6%9C%BA&ev=exbrand_%E5%8D%8E%E4%B8%BA%EF%BC%88HUAWEI%EF%BC%89%5E&
 *  page=1&s=57&click=0
 *
 *  其次我们查看第二页的地址
 *  https://search.jd.com/Search?keyword=%E5%8D%8E%E4%B8%BA%E6%89%8B%E6%9C%BA&qrst=1&suggest=1.def.0.0&wq=%E5%8D%8E%E4%B8%BA%E6%89%8B%E6%9C%BA&ev=exbrand_%E5%8D%8E%E4%B8%BA%EF%BC%88HUAWEI%EF%BC%89%5E&
 *  page=3&s=56&click=0
 *
 *  我们观察到首页的page=1,第二页的page=3,由此我们知道第i页的page参数为page=2*i-1,参数间隔为2
 *  如果我们想要抓取到100页的数据，只需要编写一个for循环，获取当前每一页的数据，改变page参数即可，获取到每个页面的html页面之后，我们需要解析
 *  当前获取的html页面，然后将其中的我们需要的内容封装到Item对象中，最后序列化到本地磁盘上
 *
 *
 */
@SuppressWarnings("all")
@Component // 将任务对象放置到Spring容器中，SpringBoot会自动调用该对象
public class CrawlerItemTask {

    private final int CRAWLER_PAGE_COUNT = 5; // 定义每次抓取的页面数量，设置为5页

    // 注入Http工具类
    @Autowired
    private HttpUtils httpUtils;

    // 注入ItemService类
    @Autowired
    private ItemService itemService;

    // 创建ObjectMapper对象来实现json数据类型的解析
    private static final ObjectMapper mapper = new ObjectMapper();


    /**
     *  实现一个定时任务，定时抓取最新的数据
     */
    @Scheduled(fixedDelay = 100 * 1000) // 该注解的作用就是间隔多长时间进行下一次任务，单位为毫秒
    public void ItemTask() {
        // 定义基础网页路径,注意此时的page参数设置为空
        String basePath = "https://search.jd.com/Search?keyword=%E5%8D%8E%E4%B8%BA%E6%89%8B%E6%9C%BA&qrst=1&suggest=1.def.0.0&wq=%E5%8D%8E%E4%B8%BA%E6%89%8B%E6%9C%BA&ev=exbrand_%E5%8D%8E%E4%B8%BA%EF%BC%88HUAWEI%EF%BC%89%5E&s=56&click=0&page=";

        // 循环获取每次需要抓取的页数
        for (int i = 1; i < CRAWLER_PAGE_COUNT * 2; i += 2) {
            // 拼接当前页面的路径
            String curHtmlPath = basePath + i;
            // 获取当前地址的html页面 错误：https://passport.jd.com/uc/login 获取的是登录页面的地址
            String curhtml = httpUtils.doGetHtml(curHtmlPath);
            // 解析html页面
            this.HtmlParse(curhtml);
        }
    }

    /**
     *  实现html页面的解析工作
     *  对于一个商品而言，其spu是唯一的，但是sku却不一定唯一，因为该商品可能有不同的参数
     *  所以我们先获取spu，通过spu来获取所有的sku
     */
    public void HtmlParse(String html) {
        // 通过Jsoup将html页面转化为DOM对象，方便我们解析html页面
        Document document = Jsoup.parse(html);

        // 通过DOM对象获取spu,方式不唯一，这里使用选择器实现获取(方便)
        // 查看通过网页工具查看html页面可知在id=J_goodsList下存放着我们需要的当前页面所有的商品信息
        // 最终放回的是当前页面所有商品的信息，并封装到Elements对象中
        Elements spuElements = document.select("div#J_goodsList > ul > li");

        // 扫描每一个商品
        for (Element spuElement : spuElements) {
            // 获取当前的spu,获取到的是字符串，但是最终存放到数据库中的类型为Long，所以需要手动转型
            Long spu = Long.parseLong(spuElement.attr("data-spu"));

            // 通过spu来获取当前商品的所有的sku信息 ,查看网页信息
            Elements skuElements = spuElement.select("li.ps-item");

            // 扫描当前商品的所有的sku信息
            for (Element skuElement : skuElements) {
                // 与上述一样，通过属性选择出sku
                Long sku = Long.parseLong(skuElement.select("[data-sku]").attr("data-sku"));

                // 采集商品的数据，并保存到数据库中，在采集之前我们需要判断一下当前商品是否已经被保存过了
                // 创建item对象
                Item item = new Item();
                // 设置其sku属性,注意不能设置spu，因为spu的范围大，sku的范围小
                item.setSku(sku);
                // 调用ItemService进行查找,返回一个list
                List<Item> items = itemService.find(item);
                // 判断
                if (items.size() >= 1) {
                    continue; // 如果大于等于1，则表明该商品已经被保存在数据库中，跳过当前循环
                }
                // 采集数据
                // 商品详情的url地址,通过查看网页信息，知道详情的url地址为https://item.jd.com/60076267214.html，其中的数字为sku
                // 只需要通过字符串的拼接即可实现，前缀是相同的
                String itemUrl = "https://item.jd.com/" + sku + ".html";
                item.setUrl(itemUrl);

                // 设置商品的图片 注意网页看到的属性未必就是我们获取的，可能不同，需要debug查看一下，此外获取的路径缺少http协议 空指针异常null
                // 空指针的异常原因：HttpGet对象没有传入Url地址，所以一直抛出空指针
                String picUrl = "https:" + skuElement.select("img[data-sku]").first().attr("data-lazy-img");
                // 修改图片大小，将地址中的n7修改为n1
                picUrl = picUrl.replace("n7","n1");
                // 下载图片,调用自定义的Http工具类实现即可
                String image = httpUtils.doGetImage(picUrl);
                item.setPic(image);

                // 设置商品的标题
                String itemInfo = httpUtils.doGetHtml(item.getUrl()); // 获取商品的详情地址
                Document itemInfoDom = Jsoup.parse(itemInfo);
                String title = itemInfoDom.select("div.sku-name").text();
                item.setTitie(title);

                // 为了获取价格，由于同一种商品的不同规格对应的不同价格，所以我们需要通过sku来获取
                // 实际上查看页面代码通过浏览器工具也可以看出来，前端通过发起js请求来查询不同的sku，从而响应不同的数据
                // https://p.3.cn/prices/mgets?skuIds=J_63215244340 ，我们只需要改变结尾的sku码即可
                // 通过查看可知，前端对应的数据为json格式的，而价格在json数据中，所以我们需要解析json数据类型，通过ObjectMapper实现
                // 我们还是通过工具类实现价格的获取
                String priceJson = httpUtils.doGetHtml("https://p.3.cn/prices/mgets?skuIds=J_" + sku);
                try {
                    double price = mapper.readTree(priceJson).get(0).get("p").asDouble();
                    item.setPrice(price);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // 设置商品的创建时间，存入数据库的时间
                item.setCreated(new Date());

                // 设置商品的更新时间，初始化为创建时间
                item.setUpdated(item.getCreated());

                // 调用ItemService实现item对象的保存
                itemService.save(item);
            }
        }
    }
}
